﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using Microsoft.Bing.Service;

namespace Microsoft.Bing
{
    /// <summary>
    /// Implements a singleton for aggregating observable data objects, search related events, global search settings and other
    /// common runtime resources. 
    /// </summary>
    /// <remarks>SearchEnvironment is a <see cref="DependencyObject"/> and also implements <see cref="INotifyPropertyChanged"/>.
    /// This allows it to be used in binding syntax.</remarks>
    /// <example>
    /// <para>
    /// The Microsoft Bing SDK is very easy to use in terms of Xaml implementation and binding to the Search data itself. The <see cref="Microsoft.Bing.SearchEnvironment"/> singleton is where
    /// the bulk of the logic and data operations take place. This object is also a <see cref="System.Windows.DependencyObject"/> and implements
    /// <see cref="System.ComponentModel.INotifyPropertyChanged"/>, which provides support for data binding. 
    /// </para>
    /// <para>
    /// To execute a search, you only need to create a <see cref="Microsoft.Bing.Service.SearchRequest"/>, populate its Query property, and
    /// then insure that you have set the SearchMedia property on the <see cref="Microsoft.Bing.SearchEnvironment"/>. Then, call the method
    /// <see cref="Microsoft.Bing.SearchEnvironment.BeginSearch"/>, insuring that you have provided a handler for <see cref="Microsoft.Bing.SearchEnvironment.SearchCompleted"/>.
    /// </para>
    /// <para>
    /// You should also bind any display controls - typically <see cref="System.Windows.Controls.ItemsControl"/> instances, such as ListBox etc - 
    /// to the <see cref="SearchEnvironment.Results"/> property. This property is of type <see cref="Microsoft.Bing.SearchResults"/> and is an observable collection.
    /// This means that you can data bind your <see cref="System.Windows.Controls.ItemsControl"/> instances in advance, like this:
    /// </para>
    /// <code>
    /// //Assumes a ItemsControl named "MyList" exists...
    /// MyList.ItemsSource = SearchEnvironment.Default.Results;
    /// 
    /// //Consume SearchEnvironment Events
    /// SearchEnvironment.Default.SearchStarted += new EventHandler(OnSearchStarted);
    /// SearchEnvironment.Default.SearchCompleted += new EventHandler&lt;SearchResponseEventArgs&gt;(OnSearchCompleted);
    /// SearchEnvironment.Default.Error += new EventHandler&lt;ErrorEventArgs&gt;(OnSearchError);
    /// 
    /// //Begin the Search (Assume a TextBox named "QueryText" exists)
    /// SearchEnvironment.Default.PageSize = 20;
    /// SearchEnvironment.Default.MediaType = SearchMedia.Web;
    /// SearchEnvironment.Default.BeginSearch(QueryText.Text);
    /// </code>
    /// <para>
    /// This should be done prior to executing any searches. Then when a search completes, the <see cref="SearchEnvironment.Results"/> collection will
    /// become populated, if there are avialable results, and your <see cref="System.Windows.Controls.ItemsControl"/> control will automatically data bind
    /// and use the asynchronous layout framework in the Silverlight runtime.
    /// </para>
    /// </example>
    /// <seealso cref="System.Windows.Controls.ItemsControl"/>
    public class SearchEnvironment : DependencyObject, INotifyPropertyChanged
    {
        /// <summary>
        /// Event which fires when a Search is begun
        /// </summary>
        public event EventHandler                          SearchStarted = null;

        /// <summary>
        /// Event which fires when a search is completed
        /// </summary>
        public event EventHandler<SearchResponseEventArgs> SearchCompleted = null;

        /// <summary>
        /// Event which fires when an error occurs during a search call
        /// </summary>
        public event EventHandler<ErrorEventArgs> Error = null;

        private SearchEnvironment()
        {
            this.PageSize = 20;
            this.HistoryDepth = 50;
        }

        private static SearchEnvironment    _Instance = null;

        /// <summary>
        /// Gets the singleton instance of the SearchEnvironment type.
        /// </summary>
        public static SearchEnvironment Default
        {
            get
            {
                if (null == _Instance) _Instance = new SearchEnvironment();
                return _Instance;
            }
        }

        private SearchEngine        _Engine;
        private SearchResults       _Results = new SearchResults();
        private uint                _CurrentPage = 1;
        private uint                _ResultCount;

        //
        //
        /* YOUR APPID GOES HERE */
        private string              _ApplicationId = "Your AppID goes here - get it at http://www.bing.com/developers";
        //
        //
        //

        private string              _Market = SearchMarkets.ENGLISH_UNITEDSTATES;
        private string              _UILanguage = UILanguages.English;
        private AdultOption         _AdultOption = AdultOption.Strict;
        private SimpleVersion       _ApiVersion = new SimpleVersion(2, 0);
        private string              _FileTypeFilter = String.Empty;
        private NewsSortOption      _NewsSort = NewsSortOption.Relevance;
        private PhonebookSortOption _PhonebookSort = PhonebookSortOption.Default;
        private SearchRequest       _CurrentRequest;    //Used for Paging...
        private SearchMedia         _MediaType = SearchMedia.Web;

        //Item Activation
        private bool                _AlwaysOpenInNewWindow = true;

        //News Locale OR Category
        private string              _LocationOverride = String.Empty;
        private string              _NewsCategory = String.Empty;

        //Hack for Opening in new tabs
        private HtmlElement ghostAnchor = null;

        //Search History
        private ISearchStorage      _SearchStorage = new IsolatedSearchStorage();
        private SearchHistory       _History = null;

        //Dependency Properties
        /// <summary>
        /// Identifies the <see cref="PageSize"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty PageSizeProperty = DependencyProperty.Register("PageSize", typeof(uint), typeof(SearchEnvironment), new PropertyMetadata(new PropertyChangedCallback(OnPageSizePropertyChanged)));

        /// <summary>
        /// Identifies the <see cref="HistoryDepth"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty HistoryDepthProperty = DependencyProperty.Register("HistoryDepth", typeof(int), typeof(SearchEnvironment), new PropertyMetadata(new PropertyChangedCallback(OnHistoryDepthPropertyChanged)));

        private static void OnPageSizePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (obj is SearchEnvironment)
            {
                ((SearchEnvironment)obj).PageSize = (uint)e.NewValue;
            }
        }

        private static void OnHistoryDepthPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (obj is SearchEnvironment)
            {
                ((SearchEnvironment)obj).HistoryDepth = (int)e.NewValue;
            }
        }

        /// <summary>
        /// Gets an instance of an <see cref="ISearchEngine"/> which defines an in-process implementation of a search provider.
        /// </summary>
        public ISearchEngine SearchEngine
        {
            get
            {
                if (_Engine == null)
                {
                    _Engine = new SearchEngine();
                    _Engine.SearchCompleted += new EventHandler<SearchResponseEventArgs>(OnSearchCompleted);
                    _Engine.Error += new EventHandler<ErrorEventArgs>(OnError);
                }

                return _Engine;
            }
        }

        /// <summary>
        /// Gets a boolean value indicating whether a search is active
        /// </summary>
        public bool HasActiveSearch
        {
            get { return _CurrentRequest != null; }
        }

        private void Requery()
        {
            if (_CurrentRequest != null)
            {
                BeginSearch(_CurrentRequest.Query);
            }
        }

        private void OnSearchStarted()
        {
            if (this.SearchStarted != null) this.SearchStarted(this, EventArgs.Empty);
        }

        /// <summary>
        /// Begins an asynchronous search query
        /// </summary>
        /// <param name="query"></param>
        public void BeginSearch(string query)
        {
            try
            {
                OnSearchStarted();

                SearchRequest request = new SearchRequest();
                request.Query = query;
                
                DecorateRequest(request);

                switch (_MediaType)
                {
                    case SearchMedia.Web:
                        SearchRequestor.DecorateWebRequest(request);
                        break;
                    case SearchMedia.Images:
                        SearchRequestor.DecorateImageRequest(request);
                        break;
                    case SearchMedia.Video:
                        SearchRequestor.DecorateVideoRequest(request);
                        break;
                    case SearchMedia.News:
                        SearchRequestor.DecorateNewsRequest(request);
                        break;
                    case SearchMedia.Phonebook:
                        SearchRequestor.DecoratePhonebookRequest(request);
                        break;
                }

                _CurrentRequest = request;

                //Record the Search in History if needed
                RecordSearch(request);

                //Begin the Search
                this.SearchEngine.BeginSearch(request);
            }
            catch (Exception ex)
            {
                OnError(ex);
            }
        }

        private void DecorateRequest(SearchRequest request)
        {
            //Required Properties
            request.AppId = this.ApplicationId;
            request.UILanguage = this.UILanguage;

            // Common request fields (optional)
            request.Version = this.ApiVersion.ToString();
            request.Market = this.Market;
            request.Adult = this.AdultOption;
            request.AdultSpecified = true;
            request.Options = new SearchOption[]
            {
                SearchOption.DisableLocationDetection
            };
        }

        #region Search History

        /// <summary>
        /// Gets/Sets the <see cref="ISearchStorage"/> interface for the <see cref="SearchEnvironment"/>
        /// </summary>
        public ISearchStorage SearchStorage
        {
            get 
            { 
                if (null == _SearchStorage) _SearchStorage = new IsolatedSearchStorage();
                return _SearchStorage; 
            }
            set { _SearchStorage = value; }
        }

        /// <summary>
        /// Gets/Sets the maximum depth of the <see cref="SearchHistory"/> tracked by the <see cref="SearchEnvironment"/>.
        /// </summary>
        public int HistoryDepth
        {
            get { return (int)GetValue(HistoryDepthProperty); }
            set 
            { 
                SetValue(HistoryDepthProperty, value); 
                TrimHistoryDepth();
            }
        }

        /// <summary>
        /// Gets the current <see cref="SearchHistory"/> for the active user.
        /// </summary>
        public SearchHistory History
        {
            get
            {
                if (_History == null)
                {
                    _History = this.SearchStorage.ReadHistory();
                }

                if (_History == null) _History = new SearchHistory();
                return _History;
            }
        }

        private void TrimHistoryDepth()
        {
            SearchHistory history = this.History;

            while (history.Count > this.HistoryDepth)
            {
                history.RemoveAt(history.Count - 1);
            }
        }

        private void RecordSearch(SearchRequest request)
        {
            SearchHistory history = this.History;

            bool exists = false;

            if (history != null)
            {
                for (int index = 0; index < history.Count; index++)
                {
                    if (String.Compare(history[index].Query, request.Query, StringComparison.CurrentCulture) == 0)
                    {
                        history[index].LastUsed = DateTime.Now;
                        exists = true;
                    }
                }

                if (!exists)
                {
                    history.Insert(0, new SearchHistoryItem(request.Query));
                    TrimHistoryDepth();
                }

                this.SearchStorage.WriteHistory(history);
            }
        }

        #endregion

        /// <summary>
        /// Gets/Sets the application Id associated with the application. This ID is assigned via the Microsoft Bing management web interfaces
        /// for developers.
        /// </summary>
        public string ApplicationId
        {
            get { return _ApplicationId; }
            set { _ApplicationId = value; }
        }

        /// <summary>
        /// Gets/Sets a simple version for the Bind API to be used.
        /// </summary>
        public SimpleVersion ApiVersion
        {
            get { return _ApiVersion; }
            set { _ApiVersion = value; }
        }

        /// <summary>
        /// Gets/Sets a string indentifier for the global market targetted for search.
        /// </summary>
        public string Market
        {
            get { return _Market; }
            set { _Market = value; }
        }

        /// <summary>
        /// Gets/Sets a string indentifier for the UI language. This determines the formatting relative to language
        /// for the search results.
        /// </summary>
        public string UILanguage
        {
            get { return _UILanguage; }
            set { _UILanguage = value; }
        }

        /// <summary>
        /// Gets/Sets a value which indicates how Adult content screening will be handled.
        /// </summary>
        public AdultOption AdultOption
        {
            get { return _AdultOption; }
            set { _AdultOption = value; }
        }

        /// <summary>
        /// Gets/Sets the file type filter used in web searches
        /// </summary>
        public string FileTypeFilter
        {
            get { return _FileTypeFilter; }
            set { _FileTypeFilter = value; }
        }

        /// <summary>
        /// Gets/Sets the location override used in searches. By default, the location is sensed by IP address - this value will cause
        /// the search to use a specific location instead.
        /// </summary>
        public string LocationOverride
        {
            get { return _LocationOverride; }
            set { _LocationOverride = value; }
        }

        /// <summary>
        /// Gets/Sets the news category used in News searches
        /// </summary>
        public string NewsCategory
        {
            get { return _NewsCategory; }
            set { _NewsCategory = value; }
        }

        /// <summary>
        /// Gets/Sets the default news sorting used by news searches
        /// </summary>
        public NewsSortOption NewsSort
        {
            get { return _NewsSort; }
            set { _NewsSort = value; }
        }

        /// <summary>
        /// Gets/Sets the default sorting used by Phonebook searches
        /// </summary>
        public PhonebookSortOption PhonebookSort
        {
            get { return _PhonebookSort; }
            set { _PhonebookSort = value; }
        }

        /// <summary>
        /// Gets/Sets a boolean value which determines if search results will always be opened in a new window when their activation links are clicked
        /// </summary>
        public bool AlwaysOpenInNewWindow
        {
            get { return _AlwaysOpenInNewWindow; }
            set { _AlwaysOpenInNewWindow = value; }
        }

        /// <summary>
        /// Gets the <see cref="System.TimeSpan"/> value from the <see cref="ISearchEngine.QueryTimeElapsed"/> property. This returns the amountt of time consumed by the last search.
        /// </summary>
        public TimeSpan QueryTimeElapsed
        {
            get { return this.SearchEngine.QueryTimeElapsed; }
        }

        void OnError(Exception ex)
        {
            if (null != this.Error) this.Error(this, new ErrorEventArgs(ex, false, null));
        }

        void OnError(object sender, ErrorEventArgs e)
        {
            if (null != this.Error) this.Error(this, e);
        }

        void OnSearchCompleted(object sender, SearchResponseEventArgs e)
        {
            //Harvest Result, if not in Error
            if (e.Response != null)
            {
                _Results.Clear();
                _ResultCount = 0;

                SearchMarshaller.MarshalResponse(e.Response, _Results, this.MediaType, true, ref _ResultCount);

                if (this.SearchCompleted != null) this.SearchCompleted(this, e);
            }
            else
            {
                OnError(new Exception("No search response found."));
            }
        }

        /// <summary>
        /// Gets/Sets a value which determines the <see cref="SearchMedia"/> to use when executing searches.
        /// </summary>
        public SearchMedia MediaType
        {
            get { return _MediaType; }
            set 
            {
                if (_MediaType != value)
                {
                    _MediaType = value;
                    Requery();
                }
            }
        }

        /// <summary>
        /// Gets/Sets a value which indicates how many total results were returned. This value can and will often be an approximation.
        /// </summary>
        public uint ResultCount
        {
            get { return _ResultCount; }
            set { _ResultCount = value; }
        }

        /// <summary>
        /// Gets a collection of the current <see cref="SearchResults"/>.
        /// </summary>
        public SearchResults Results
        {
            get { return _Results; }
        }

        /// <summary>
        /// Gets/Sets a value which defines the number of elements per page (Page Size)
        /// </summary>
        public uint PageSize
        {
            get { return (uint)GetValue(PageSizeProperty); }
            set 
            {
                if (this.PageSize != value)
                {
                    SetValue(PageSizeProperty, Math.Min(50, Math.Max(10, value)));
                    OnPropertyChanged("PageSize");
                }
            }
        }

        /// <summary>
        /// Gets a value which indicates how many pages are in the current <see cref="SearchResults"/> based on <see cref="ResultCount"/> and <see cref="PageSize"/>.
        /// </summary>
        public uint PageCount
        {
            get
            {
                uint count = _ResultCount / this.PageSize;
                if (_ResultCount % this.PageSize != 0) count++;
                return count;
            }
        }

        /// <summary>
        /// Gets a value which indicates the current page of the active search
        /// </summary>
        public uint CurrentPage
        {
            get { return _CurrentPage; }
        }

        /// <summary>
        /// Pages the current search to a specific page
        /// </summary>
        /// <remarks>NOTE: Page Indexes are one to N cardinality - not zero to N - 1.</remarks>
        /// <param name="pageIndex">The desired page index</param>
        public void PageTo(uint pageIndex)
        {
            if (pageIndex >= 1 && pageIndex <= this.PageCount)
            {
                _CurrentPage = pageIndex;
                Requery();
            }
        }

        /// <summary>
        /// Pages to the first page in the current search
        /// </summary>
        public void PageFirst()
        {
            if (_CurrentPage > 1)
            {
                _CurrentPage = 1;
                Requery();
            }
        }

        /// <summary>
        /// Pages to the previous page, if possible, in the current search
        /// </summary>
        public void PageBack()
        {
            if (_CurrentPage > 1)
            {
                _CurrentPage--;
                Requery();
            }
        }

        /// <summary>
        /// Pages to the next page, if possible, in the current search
        /// </summary>
        public void PageNext()
        {
            if (_CurrentPage < this.PageCount)
            {
                _CurrentPage++;
                Requery();
            }
        }

        /// <summary>
        /// Pages to the last page, if possible, in the current search
        /// </summary>
        public void PageLast()
        {
            if (_CurrentPage < this.PageCount)
            {
                _CurrentPage = this.PageCount;
                Requery();
            }
        }

        /// <summary>
        /// Opens a search result as content - If the search result links to a Silverlight supported format,
        /// then the result will be opened in a custom preview control. This includes Portable Networks Graphics images (.PNG), Joint
        /// Photgraphics Experts images (.JPG), which will be shown in an image preview Window control, and Windows Media Video which
        /// are shown in a video player window. All other types will act like a standard HTML hyperlink, opening the content in a window.
        /// The newWindow boolean arugment determines whether the content will be opened in a new window, or reuse the current window.
        /// The newWindow argument is ignored if the <c href="SearchEnvironment"/> property AlwaysOpenInNewWindow is set to true.
        /// </summary>
        /// <param name="result">The <c href="SearchResult"/> to open</param>
        /// <param name="newWindow">A boolean flag indicating whether the result is to opened in a new browser window or the current window</param>
        public void OpenSearchResult(SearchResult result, bool newWindow)
        {
            Uri uri = new Uri(result.DisplayUrl);
            if (newWindow || this.AlwaysOpenInNewWindow)
            {
                HtmlElement ghostLink = this.GhostAnchor;
                if (ghostLink != null)
                {
                    ghostLink.SetAttribute("href", result.DisplayUrl);
                    ghostLink.SetAttribute("target", "_blank");
                    ghostLink.Invoke("click");
                }
                else
                {
                    HtmlPage.Window.Navigate(uri, "_self");
                }
            }
            else
            {
                HtmlElement ghostLink = this.GhostAnchor;
                if (ghostLink != null)
                {
                    ghostLink.SetAttribute("href", result.DisplayUrl);
                    ghostLink.SetAttribute("target", "_self");
                    ghostLink.Invoke("click");
                }
                else
                {
                    HtmlPage.Window.Navigate(uri);
                }
            }
        }

        private HtmlElement GhostAnchor
        {
            get
            {
                if (ghostAnchor == null)
                {
                    ghostAnchor = HtmlPage.Document.CreateElement("a");
                    ghostAnchor.SetAttribute("target", "_blank");
                    ghostAnchor.SetStyleAttribute("display", "none");
                    HtmlPage.Document.Body.AppendChild(ghostAnchor);
                }

                return ghostAnchor;
            }
        }

        /// <summary>
        /// Handler which fires the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">The name of the property which changed</param>
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Event which fires when Dependency Properties have changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }
}
